//编译模块 : 只负责进行代码的编译
#pragma once

#include <iostream>
#include <cstdlib>     //exit()
#include <unistd.h>    //fork()/close()
#include <sys/wait.h>  //wait()
#include <sys/types.h> //open()
#include <sys/stat.h>  //open()
#include <fcntl.h>     //open()

#include "../common/util.hpp" //引入common目录下的工具模块
#include "../common/log.hpp"  //引入common目录下的日志模块

//放入命名空间的好处：1.防止命名紊乱、2.方便后续找到类、函数的所在位置
namespace ns_compiler
{
    using namespace ns_util;//展开工具模块
    using namespace ns_log; //展开日志模块

    //类里面只有一个方法，为什么还需要进行这层类的封装？ 
    //--模块化，将所有相关联的功能放在一个模块中，一个功能一个模块
    class Compiler
    {
    public:
        Compiler(){}
        ~Compiler(){}

        /***************************************************************************
         * 编译函数：实现编译功能的主体。通过创建子进程，子进程调用程序替换函数来完成编译。
         *          编译成功与否，父进程通过查看是否生成对应的可执行程序文件来判断。
         * 输入参数：一个文件名，我们能根据该文件名拼接出要编译的文件的文件名
         * 返回值：编译成功：true，否则：false
         ***************************************************************************/
        static bool Compile(const std::string &file_name)
        {
            /**************************************************************************************************
             * 编译过程中要根据文件名生成三个文件名。
             * file_name: 1234 （传进来的是文件名不是文件，只有文件名没有后缀）
             * 根据文件名拼接出以下文件名：
             * 1234 -> ./temp/1234.cpp   （要编译的文件的文件名,通过它找到要编译的文件）
             * 1234 -> ./temp/1234.exe   （程序名,通过它命名生成可执行程序）
             * 1234 -> ./temp/1234.stderr（标准错误文件的文件名,通过它命名生成的标准错误文件，用来保存程序编译时的错误）
             **************************************************************************************************/
            //1.创建子进程
            pid_t pid = fork();
            if(pid < 0) //创建子进程失败，给部署程序的主机打印提示。
            {
                LOG(ERROR) << "创建子进程失败，编译模块无法再向下执行。" << "\n";
                return false;
            }
            else if (pid == 0) //创建子进程成功，子进程调用程序替换函数来完成编译。
            {
            //2.创建并打开编译错误文件并进行标准错误重定向。
                //设置系统掩码，调整新建文件的起始权限。
                umask(0);
                //通过系统调用open()创建并打开 编译错误文件，用来保存程序编译时的错误。
                int _stderr = open(PathUtil::CompilerError(file_name).c_str(), O_CREAT | O_WRONLY, 0644);
                if(_stderr < 0) //打开编译错误文件失败，退出
                {
                    LOG(WARNING) << "没有成功形成stderr文件，后续需要处理。" << "\n";
                    exit(1); 
                }
                else //打开编译错误文件成功，进行标准错误重定向
                {
                    //重定向标准错误到_stderr，这样系统会自动将编译错误打印到文件中。
                    /*dup(oldfd, newfd): 将oldfd保存的文件指针拷贝到newfd中*/
                    dup2(_stderr, 2);
                }
                
            //3.使用程序替换函数，调用编译器，找到要编译的文件，完成对代码的编译工作。(可能编译失败，所以需要将失败信息保存到文件中)
                /**************************************************************************
                 * execlp(程序路径，argv选项，NULL);
                 *     程序路径 : g++(g++程序在任何路径下都可执行)
                 *     argv选项 : g++ -o 生成的可执行程序的文件名 要编译的文件的文件名 -std=c++11
                 **************************************************************************/
                execlp("g++", "g++", "-o", PathUtil::Exe(file_name).c_str(),\
                PathUtil::Src(file_name).c_str(), "-D", "COMPILER_ONLINE","-std=c++11",  nullptr/*不要忘记*/);
                LOG(ERROR) << "启动编译器g++失败，编译模块无法再向下执行。" << "\n";
                close(_stderr);
                exit(2);
            }
            else 
            {
            //4.父进程通过查看是否生成对应的可执行程序文件来判断编译成功与否
                waitpid(pid, nullptr, 0); //父进程等待子进程退出
                //编译是否成功,就看有没有形成对应的可执行程序。通过调用工具模块中文件工具类中的IsFileExists方法来判断。
                if(FileUtil::IsFileExists(PathUtil::Exe(file_name))){
                    LOG(INFO) << PathUtil::Src(file_name) << " 编译成功!" << "\n";
                    return true;
                }
            }
            LOG(FATAL) << "编译失败，没有形成可执行程序，OJ系统无法再继续运行。" << "\n";
            return false;
        }
    };
}